package com.caseeye.base
import com.caseeye.base.Imports._
import com.caseeye.exception.ConcurrencyException
import com.caseeye.util.{ RichDomainObjectFactory, ClassUtils }
import com.novus.casbah.mongodb.Imports._
import javax.annotation.Resource
import java.io.Serializable
import java.lang.reflect.{ ParameterizedType }
import java.util.Date
import org.bson.types.ObjectId
import org.springframework.beans.factory.annotation.Required
import org.springframework.util.StringUtils._
import scala.reflect.BeanProperty

trait GenericDAO[T <: Entity] extends Logging {

    @BeanProperty
    @Resource
    @Required
    var mongoDB : MongoDB = null

    @BeanProperty
    @Resource
    @Required
    var richDomainObjectFactory : RichDomainObjectFactory = null

    val genericType =
        this.getClass.getGenericInterfaces
            .filter(intf => intf.isInstanceOf[ParameterizedType]
                && intf.asInstanceOf[ParameterizedType].getRawType == classOf[GenericDAO[T]])
            .flatMap(intf => intf.asInstanceOf[ParameterizedType].getActualTypeArguments
                .filter(at => classOf[Entity].isAssignableFrom(at.asInstanceOf[Class[_]]))
                .map(at => at.asInstanceOf[Class[T]])).apply(0)

    protected def getGenericType : Class[T] = genericType

    protected def getSchema : String = getGenericType.getSimpleName

    protected def getColl : MongoCollection = mongoDB(getSchema)

    def findAll : List[T] = for (dbObj <- getColl.toList) yield dbObj2Entity(dbObj, getGenericType)

    def findById(id : Serializable) : Option[T] = {
        if (id == null || !hasText(id.toString)) throw new RuntimeException("id cannot be null or empty")
        try {
            val re = getColl.findOne(MongoDBObject(MongoIdField -> new ObjectId(id.toString)))
            if (re == None)
                None
            else {
                Some(dbObj2Entity(re.get, getGenericType))
            }
        } catch {
            case ex : IllegalArgumentException if (ex.getMessage.matches("""\s*invalid ObjectId.*""")) => None
            case e => throw e
        }
    }

    def findById(id : Serializable, orElse : T) : T = {
        val re = findById(id)
        if (re == None) richDomainObjectFactory.autowire(orElse) else re.get
    }

    def save(t : T) : Boolean = {
        t match {
            case e : Versioning => {
                val x = checkVersion(e)
                x.versionUp
                getColl.save(x.asInstanceOf[T])
                true
            }
            case e : Entity => getColl.save(t); true
            case _ => {
                if (log.isDebugEnabled) {
                    log.debug("Invalid entity to save[%s]".format(t))
                }
                false
            }
        }
    }

    private def getIdFieldValue[T <% AnyRef](t : T) : AnyRef = ClassUtils.getValueForField(t, ClassUtils.getIdField(t.getClass).get)

    private def checkVersion[V <% Versioning](t : V) : V = {
        val id = getIdFieldValue(t)
        id match {
            case idValue : String => {
                findById(id.asInstanceOf[Serializable]) match {
                    case Some(record) => {
                        if (t.olderThan(record.asInstanceOf[Versioning])) {
                            throw new ConcurrencyException(record)
                        } else {
                            t
                        }
                    }
                    case None => t
                }
            }
            case _ => t
        }
    }

    def remove(t : T) : Boolean = {
        if (t != null && t != None) {
            getColl.remove(t)
            true
        } else {
            if (log.isDebugEnabled) {
                log.debug("entity is empty")
            }
            false
        }
    }

    def query[Q](q : Q, pageNo : Int = -1, pageSize : Int = -1, sort : MongoDBObject = MongoDBObject()) : List[T] = {
        q match {
            case e : MongoDBObject => queryInternal(e)(pageNo, pageSize)(sort)
            case e : Map[String, AnyRef] => queryInternal(MongoDBObject() ++= e)(pageNo, pageSize)(sort)
            case e : Seq[Tuple2[String, AnyRef]] => queryInternal(MongoDBObject() ++= e)(pageNo, pageSize)(sort)
            case e : AnyRef if !e.getClass.isPrimitive
                && !e.isInstanceOf[String]
                && !e.isInstanceOf[Date] => queryInternal(entity2QueryObj(e))(pageNo, pageSize)(sort)
            case _ => throw new RuntimeException("Not a valid query object[%s]".format(q))
        }
    }

    def queryOne[Q](q : Q, sort : MongoDBObject = MongoDBObject()) : Option[T] = query(q, pageNo = 1, pageSize = 1, sort = sort).find(_ != null)

    private def calculatePagingInfo(totalCount : Number, pageNo : Int, pageSize : Int) : Tuple2[Int, Int] = {
        val skip = 0
        val limit = totalCount.intValue
        if (pageSize <= 0) return (skip, limit)

        val pageCount = scala.math.ceil(totalCount.doubleValue / pageSize).intValue
        //(skip,limit)
        (((pageNo max 0 min pageCount) - 1 max 0) * pageSize, pageSize)
    }

    private def queryInternal(q : DBObject)(pageNo : Int, pageSize : Int)(sort : MongoDBObject) : List[T] = {
        val pagingInfo = calculatePagingInfo(getColl.count(q), pageNo, pageSize)
        val skip = pagingInfo._1
        val limit = pagingInfo._2
        log.debug("Skip=%s, Limit=%s".format(skip, limit))
        (for (dbObj <- getColl.find(q).sort(sort).skip(skip).limit(limit)) yield dbObj2Entity(dbObj, getGenericType)).toList
    }
}
